# !/usr/local/bin/python3
# -*- encoding: utf-8 -*-
# @FileName :   main.py
# @DateTime :   2022/7/14 | 12:32
# @Author   :   IoTus3
# @GitHub   :   https://gitee.com/IoTus3
# @Contact  :   xianglin.fu3@gmail.com
# @License  :   (C)Copyright 2022-2023, LinYuStudio-NLPR-CASIA
# @Editor   :   Power by PyCharm
# @Desc     :   None


# IMPORT SYSTEM LIB
import shutil
import sys
import os
import datetime
import platform
import time
import traceback

# IMPORT PYSIDE2
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *

# IMPORT PYSIDE2 EXTENSION
import PySide2extn

# IMPORT MATERIAL
from qt_material import *

# IMPORT PSUTIL
import psutil

# IMPORT UI INTERFACE LIB
from ui_interface import *

# define the platforms
platforms = {
    "linux": "Linux",
    "linux1": "Linux",
    "linux2": "Linux",
    "darwin": "Mac OS X",
    "win32": "Windows"
}


# worker signal class
class WorkerSignals(QObject):
    finished = Signal()
    error = Signal(tuple)
    result = Signal(object)
    progress = Signal(int)


# worker class
class Worker(QRunnable):
    def __init__(self, fn, *args, **kwargs):
        super(Worker, self).__init__()

        self.fn = fn
        self.args = args
        self.kwargs = kwargs
        self.signals = WorkerSignals()

        self.kwargs['progress_callback'] = self.signals.progress

    @Slot()
    def run(self):
        try:
            result = self.fn(*self.args, **self.kwargs)
        except:
            traceback.print_exc()
            exctype, value = sys.exc_info()[:2]
            self.signals.error.emit((exctype, value, traceback.format_exc()))
        else:
            self.signals.result.emit(result)
        finally:
            self.signals.finished.emit()  # Done


# main window class
class MainWindow(QMainWindow):
    def __init__(self):
        QMainWindow.__init__(self)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)

        # apply the beautiful style
        apply_stylesheet(app, theme="light_cyan_500.xml")

        # remove window title bar
        self.setWindowFlag(Qt.FramelessWindowHint)

        # shadow effect style
        self.shadow = QGraphicsDropShadowEffect(self)
        self.shadow.setBlurRadius(50)
        self.shadow.setXOffset(0)
        self.shadow.setYOffset(0)
        self.shadow.setColor(QColor(0, 92, 157, 550))

        # apply shadow to central widget
        self.ui.centralwidget.setGraphicsEffect(self.shadow)

        # set window icons
        self.setWindowIcon(QIcon(":/icons/icons/airplay.svg"))

        # set window title
        self.setWindowTitle("IoTus3 Monitor")

        # window size grip to resize window
        QSizeGrip(self.ui.block_frame)

        # minimize_window_button
        self.ui.minimize_window_button.clicked.connect(lambda: self.showMinimized())

        # close window button
        self.ui.close_window_button.clicked.connect(lambda: self.close())

        # restore/maximize window button
        self.ui.restore_window_button.clicked.connect(lambda: self.restore_or_maximize_window())

        # stacked pages navigation
        # Using side menu buttons

        # navigation to cpu page
        self.ui.cpu_button.clicked.connect(lambda: self.ui.stackedWidget.setCurrentWidget(self.ui.page_cpu_and_memory))

        # navigation to battery page
        self.ui.battery_button.clicked.connect(lambda: self.ui.stackedWidget.setCurrentWidget(self.ui.page_battery))

        # navigation to system page
        self.ui.system_button.clicked.connect(lambda: self.ui.stackedWidget.setCurrentWidget(self.ui.page_system))

        # navigation to activity page
        self.ui.activity_button.clicked.connect(lambda: self.ui.stackedWidget.setCurrentWidget(self.ui.page_activity))

        # navigation to storage_button page
        self.ui.storage_button.clicked.connect(lambda: self.ui.stackedWidget.setCurrentWidget(self.ui.page_storage))

        # navigation to sensor page
        self.ui.sensor_button.clicked.connect(lambda: self.ui.stackedWidget.setCurrentWidget(self.ui.page_sensors))

        # navigation to network page
        self.ui.network_button.clicked.connect(lambda: self.ui.stackedWidget.setCurrentWidget(self.ui.page_network))

        # about me hyperlinks
        # my school gitee
        self.ui.git_lbl.setText('''<a href="https://gitee.com/IoTus3">我的校园Gitee</a>''')
        self.ui.git_lbl.setOpenExternalLinks(True)

        # tea
        self.ui.tea_lbl.setText('''<a href="https://gitee.com/IoTus3/2022-graduation-design/blob/master/README.md">请我喝杯茶</a>''')
        self.ui.tea_lbl.setOpenExternalLinks(True)

        # donate
        self.ui.donate_lbl.setText('''<a href="https://gitee.com/IoTus3/2022-graduation-design/blob/master/README.md">捐助我一下</a>''')
        self.ui.donate_lbl.setOpenExternalLinks(True)

        # question
        self.ui.question_button.clicked.connect(lambda: self.refresh_question_button())

        # function to move window
        def moveWindow(e):
            if self.isMaximized() == False:
                if e.buttons() == Qt.LeftButton:
                    # move window
                    self.move(self.pos() + e.globalPos() - self.clickPosition)
                    self.clickPosition = e.globalPos()
                    e.accept()

        # move window
        self.ui.header_frame.mouseMoveEvent = moveWindow

        # Left Menu toggle button
        self.ui.menu_button.clicked.connect(lambda: self.slideLeftMenu())

        # Style clicked menu button
        for w in self.ui.menu_frame.findChildren(QPushButton):
            # add click event listener
            w.clicked.connect(self.applyButtonStyle)

        # battery information
        # self.battery()

        # cpu and ram information
        # self.cpu_ram()

        # system information
        self.system_info()

        # processes information
        self.processes()

        # storage information
        self.storage()

        # sensor information
        self.sensors()

        # network information
        self.network()

        # start thread
        self.threadpool = QThreadPool()

        # psutil thread
        self.psutil_thread()

        # show the app window
        self.show()

    # QPushButton open url
    def refresh_question_button(self):
        issueUrl = "https://gitee.com/IoTus3/2022-graduation-design/issues"
        QDesktopServices.openUrl(QUrl(issueUrl))


    # crete psutil thread function
    def psutil_thread(self):
        # live cpu info
        worker = Worker(self.cpu_ram)

        # start worker
        worker.signals.result.connect(self.print_output)
        worker.signals.finished.connect(self.thread_complete)
        worker.signals.progress.connect(self.progress_fn)

        # Execute
        self.threadpool.start(worker)

        # live battery info
        battery_worker = Worker(self.battery)

        # start worker
        battery_worker.signals.result.connect(self.print_output)
        battery_worker.signals.finished.connect(self.thread_complete)
        battery_worker.signals.progress.connect(self.progress_fn)

        # Execute
        self.threadpool.start(battery_worker)

    # worker print out
    def print_output(self, s):
        print(s)

    # thread_complete
    def thread_complete(self):
        print("THREAD COMPLETE")

    # work thread progress function
    def progress_fn(self, n):
        print("%d%% done" % n)

    # get network information
    def network(self):
        # net stats
        for x in psutil.net_if_stats():
            z = psutil.net_if_stats()

            # create New Row
            rowPosition = self.ui.net_stats_table.rowCount()
            self.ui.net_stats_table.insertRow(rowPosition)

            self.create_table_widget(rowPosition, 0, x, "net_stats_table")
            self.create_table_widget(rowPosition, 1, str(z[x].isup), "net_stats_table")
            self.create_table_widget(rowPosition, 2, str(z[x].duplex), "net_stats_table")
            self.create_table_widget(rowPosition, 3, str(z[x].speed), "net_stats_table")
            self.create_table_widget(rowPosition, 4, str(z[x].mtu), "net_stats_table")

        # net io counters
        for x in psutil.net_io_counters(pernic=True):
            z = psutil.net_io_counters(pernic=True)

            # create new row
            rowPosition = self.ui.net_io_table.rowCount()
            self.ui.net_io_table.insertRow(rowPosition)

            self.create_table_widget(rowPosition, 0, x, "net_io_table")
            self.create_table_widget(rowPosition, 1, str(z[x].bytes_sent), "net_io_table")
            self.create_table_widget(rowPosition, 2, str(z[x].bytes_recv), "net_io_table")
            self.create_table_widget(rowPosition, 3, str(z[x].packets_sent), "net_io_table")
            self.create_table_widget(rowPosition, 4, str(z[x].packets_recv), "net_io_table")
            self.create_table_widget(rowPosition, 5, str(z[x].errin), "net_io_table")
            self.create_table_widget(rowPosition, 6, str(z[x].errout), "net_io_table")
            self.create_table_widget(rowPosition, 7, str(z[x].dropin), "net_io_table")
            self.create_table_widget(rowPosition, 8, str(z[x].dropout), "net_io_table")

        # net address
        for x in psutil.net_if_addrs():
            z = psutil.net_if_addrs()

            for y in z[x]:
                # create new row
                rowPosition = self.ui.net_address_table.rowCount()
                self.ui.net_address_table.insertRow(rowPosition)

                self.create_table_widget(rowPosition, 0, str(x), "net_address_table")
                self.create_table_widget(rowPosition, 0, str(y.family), "net_address_table")
                self.create_table_widget(rowPosition, 0, str(y.address), "net_address_table")
                self.create_table_widget(rowPosition, 0, str(y.netmask), "net_address_table")
                self.create_table_widget(rowPosition, 0, str(y.broadcast), "net_address_table")
                self.create_table_widget(rowPosition, 0, str(y.ptp), "net_address_table")

        # net connections
        for x in psutil.net_connections():
            z = psutil.net_connections()

            # create new row
            rowPosition = self.ui.net_connections_table.rowCount()
            self.ui.net_connections_table.insertRow(rowPosition)

            self.create_table_widget(rowPosition, 0, str(x.fd), "net_connections_table")
            self.create_table_widget(rowPosition, 0, str(x.family), "net_connections_table")
            self.create_table_widget(rowPosition, 0, str(x.type), "net_connections_table")
            self.create_table_widget(rowPosition, 0, str(x.laddr), "net_connections_table")
            self.create_table_widget(rowPosition, 0, str(x.raddr), "net_connections_table")
            self.create_table_widget(rowPosition, 0, str(x.status), "net_connections_table")
            self.create_table_widget(rowPosition, 0, str(x.pid), "net_connections_table")

    # get sensor information
    def sensors(self):
        # psutil sensors currently only supports on linux platform
        if sys.platform == "linux" or sys.platform == "linux1" or sys.platform == "linux2":
            for x in psutil.sensors_temperatures():
                for y in psutil.sensors_temperatures()[x]:
                    # create new row
                    rowPosition = self.ui.sensorTable.rowCount()
                    self.ui.sensorTable.insertRow(rowPosition)

                    self.create_table_widget(rowPosition, 0, x, "sensorTable")
                    self.create_table_widget(rowPosition, 1, y.label, "sensorTable")
                    self.create_table_widget(rowPosition, 2, str(y.current), "sensorTable")
                    self.create_table_widget(rowPosition, 3, str(y.high), "sensorTable")
                    self.create_table_widget(rowPosition, 4, str(y.critical), "sensorTable")

                    temp_per = (y.current / y.high) * 100

                    progressBar = QProgressBar(self.ui.sensorTable)
                    progressBar.setObjectName(u"progressBar")
                    progressBar.setValue(temp_per)
                    self.ui.sensorTable.setCellWidget(rowPosition, 5, progressBar)
        else:
            # create new row
            rowPosition = self.ui.sensorTable.rowCount()
            self.ui.sensorTable.insertRow(rowPosition)

            self.create_table_widget(rowPosition, 0, "Function not supported on " + platforms[sys.platform],
                                     "sensorTable")
            self.create_table_widget(rowPosition, 1, "N/A", "sensorTable")
            self.create_table_widget(rowPosition, 2, "N/A", "sensorTable")
            self.create_table_widget(rowPosition, 3, "N/A", "sensorTable")
            self.create_table_widget(rowPosition, 4, "N/A", "sensorTable")
            self.create_table_widget(rowPosition, 5, "N/A", "sensorTable")

    # get storage information
    def storage(self):
        storage_device = psutil.disk_partitions(all=False)
        z = 0
        for x in storage_device:
            rowPosition = self.ui.storageTable.rowCount()
            self.ui.storageTable.insertRow(rowPosition)

            self.create_table_widget(rowPosition, 0, x.device, "storageTable")
            self.create_table_widget(rowPosition, 1, x.mountpoint, "storageTable")
            self.create_table_widget(rowPosition, 2, x.fstype, "storageTable")
            self.create_table_widget(rowPosition, 3, x.opts, "storageTable")

            # check platform
            if sys.platform == "linux" or sys.platform == "linux1" or sys.platform == "linux2":
                self.create_table_widget(rowPosition, 4, str(x.maxfile), "storageTable")
                # self.create_table_widget(rowPosition, 5, str(x.maxpath), "storageTable")
            else:
                self.create_table_widget(rowPosition, 4, "Function not available on " + platforms[sys.platform],
                                         "storageTable")
                # self.create_table_widget(rowPosition, 5, "Function not available on " + platforms[sys.platform], "storageTable")

            # disk usage
            disk_usage = shutil.disk_usage(x.mountpoint)
            self.create_table_widget(rowPosition, 5,
                                     str("{:.4f}".format((disk_usage.total / (1024 * 1024 * 1024)))) + "GB",
                                     "storageTable")
            self.create_table_widget(rowPosition, 6,
                                     str("{:.4f}".format((disk_usage.free / (1024 * 1024 * 1024)))) + "GB",
                                     "storageTable")
            self.create_table_widget(rowPosition, 7,
                                     str("{:.4f}".format((disk_usage.used / (1024 * 1024 * 1024)))) + "GB",
                                     "storageTable")

            full_disk = (disk_usage.used / disk_usage.total) * 100
            progressBar = QProgressBar(self.ui.storageTable)
            progressBar.setObjectName(u"progressBar")
            progressBar.setValue(int(full_disk))
            self.ui.storageTable.setCellWidget(rowPosition, 8, progressBar)

    # create table widget
    def create_table_widget(self, rowPosition, columnPosition, text, tableName):
        qtabwidghtitem = QTableWidgetItem()
        # use getattr() METHOD
        getattr(self.ui, tableName).setItem(rowPosition, columnPosition, qtabwidghtitem)
        qtablewidgetitem = getattr(self.ui, tableName).item(rowPosition, columnPosition)
        qtablewidgetitem.setText(text)

    # get system running processes
    def processes(self):
        for x in psutil.pids():
            # create new row
            rowPosition = self.ui.tableWidget.rowCount()
            self.ui.tableWidget.insertRow(rowPosition)

            try:
                process = psutil.Process(x)

                self.create_table_widget(rowPosition, 0, str(process.pid), "tableWidget")
                self.create_table_widget(rowPosition, 1, process.name(), "tableWidget")
                self.create_table_widget(rowPosition, 2, process.status(), "tableWidget")
                self.create_table_widget(rowPosition, 3,
                                         str(datetime.datetime.utcfromtimestamp(process.create_time()).strftime(
                                             "%Y-%m-%d %H:%M:%S")), "tableWidget")

                # create an cell widget
                suspend_btn = QPushButton(self.ui.tableWidget)
                suspend_btn.setText("Suspend")
                suspend_btn.setStyleSheet("color: brown")
                self.ui.tableWidget.setCellWidget(rowPosition, 4, suspend_btn)

                resume_btn = QPushButton(self.ui.tableWidget)
                resume_btn.setText("Resume")
                resume_btn.setStyleSheet("color: green")
                self.ui.tableWidget.setCellWidget(rowPosition, 5, resume_btn)

                terminate_btn = QPushButton(self.ui.tableWidget)
                terminate_btn.setText("Terminate")
                terminate_btn.setStyleSheet("color: orange")
                self.ui.tableWidget.setCellWidget(rowPosition, 6, terminate_btn)

                kill_btn = QPushButton(self.ui.tableWidget)
                kill_btn.setText("Kill")
                kill_btn.setStyleSheet("color: red")
                self.ui.tableWidget.setCellWidget(rowPosition, 7, kill_btn)
            except Exception as e:
                print(e)

        self.ui.activity_search.textChanged.connect(self.findName)

    # search activity table
    def findName(self):
        name = self.ui.activity_search.text().lower().strip()
        for row in range(self.ui.tableWidget.rowCount()):
            item = self.ui.tableWidget.item(row, 1)
            self.ui.tableWidget.setRowHidden(row, name not in item.text().lower().strip())

    # system information
    def system_info(self):
        time = datetime.datetime.now().strftime("%I:%M:%S %p")
        self.ui.system_time.setText(str(time))
        date = datetime.datetime.now().strftime("%Y-%m-%d")
        self.ui.system_date.setText(str(date))

        self.ui.system_machine.setText(platform.machine())
        self.ui.system_version.setText(platform.version())
        self.ui.system_platform.setText(platform.platform())
        self.ui.system_processor.setText(platform.processor())

    # system cpu and ram information
    def cpu_ram(self, progress_callback):
        while True:
            # ram information
            totalRam = 1.0
            totalRam = psutil.virtual_memory()[0] * totalRam
            totalRam = totalRam / (1024 * 1024 * 1024)
            self.ui.total_ram.setText(str("{:.4f}".format(totalRam) + "GB"))

            availRam = 1.0
            availRam = psutil.virtual_memory()[1] * availRam
            availRam = availRam / (1024 * 1024 * 1024)
            self.ui.available_ram.setText(str("{:.4f}".format(availRam) + "GB"))

            ramUsed = 1.0
            ramUsed = psutil.virtual_memory()[3] * ramUsed
            ramUsed = ramUsed / (1024 * 1024 * 1024)
            self.ui.used_ram.setText(str("{:.4f}".format(ramUsed) + "GB"))

            ramFree = 1.0
            ramFree = psutil.virtual_memory()[4] * ramFree
            ramFree = ramFree / (1024 * 1024 * 1024)
            self.ui.free_ram.setText(str("{:.4f}".format(ramFree) + "GB"))

            ramUsages = str(psutil.virtual_memory()[2]) + "%"
            self.ui.ram_usage_lbl.setText(ramUsages)

            # cpu information
            core = psutil.cpu_count()
            self.ui.cpu_count.setText(str(core))

            cpuPer = psutil.cpu_percent()
            self.ui.cpu_per.setText(str(cpuPer) + "%")

            cpuMainCore = psutil.cpu_count(logical=False)
            self.ui.cpu_main_core.setText(str(cpuMainCore))

            # cpu percentage indicator
            # set progress bar value
            self.ui.cpu_usage.rpb_setMaximum(100)
            self.ui.cpu_usage.rpb_setValue(cpuPer)
            self.ui.cpu_usage.rpb_setBarStyle('Hybrid2')
            self.ui.cpu_usage.rpb_setLineColor((25, 202, 173))
            self.ui.cpu_usage.rpb_setPieColor((160, 238, 225))
            self.ui.cpu_usage.rpb_setTextColor((244, 96, 108))
            self.ui.cpu_usage.rpb_setInitialPos('West')
            self.ui.cpu_usage.rpb_setTextFormat('Percentage')
            self.ui.cpu_usage.rpb_setTextFont('Arial')
            self.ui.cpu_usage.rpb_setLineWidth(15)
            self.ui.cpu_usage.rpb_setPathWidth(15)
            self.ui.cpu_usage.rpb_setLineCap('RoundCap')

            # ram percentage indicator
            # set progress bar value
            self.ui.ram_usage.spb_setMinimum((0, 0, 0))
            self.ui.ram_usage.spb_setMaximum((totalRam, totalRam, totalRam))
            self.ui.ram_usage.spb_setValue((availRam, ramUsed, ramFree))
            self.ui.ram_usage.spb_lineColor(((6, 233, 38), (6, 201, 233), (233, 6, 201)))
            self.ui.ram_usage.spb_setInitialPos(('West', 'West', 'West'))
            self.ui.ram_usage.spb_lineWidth(15)
            self.ui.ram_usage.spb_lineStyle(('SolidLine', 'SolidLine', 'SolidLine'))
            self.ui.ram_usage.spb_lineCap(('RoundCap', 'RoundCap', 'RoundCap'))
            self.ui.ram_usage.spb_setPathHidden(True)

            # sleep 1 sec
            time.sleep(1)

    # function to convert seconds to hours
    def secs2hours(self, secs):
        mm, ss = divmod(secs, 60)
        hh, mm = divmod(mm, 60)
        return "%d:%02d:%02d (H:M:S)" % (hh, mm, ss)

    # get system battery information
    def battery(self, progress_callback):
        while True:
            batt = psutil.sensors_battery()

            if not hasattr(psutil, "sensors_battery"):
                self.ui.battery_status.setText("Platform not supported")

            if batt is None:
                self.ui.battery_status.setText("No battery installed")

            if batt.power_plugged:
                self.ui.battery_charge.setText(str(round(batt.percent, 2)) + "%")
                self.ui.battery_time_left.setText("N/A")
                if batt.percent < 100:
                    self.battery_status.setText("Charging")
                else:
                    self.ui.battery_status.setText("Fully Charged")

                self.ui.battery_plugged.setText("YES")
            else:
                self.ui.battery_charge.setText(str(round(batt.percent, 2)) + "%")
                self.ui.battery_time_left.setText(self.secs2hours(batt.secsleft))
                if batt.percent < 100:
                    self.ui.battery_status.setText("Discharging")
                else:
                    self.ui.battery_status.setText("Fully Charged")
                self.ui.battery_plugged.setText("NO")

            # battery power indicator using round progress bar
            # set progress bar value
            self.ui.battery_usage.rpb_setMaximum(100)
            # set progress value
            self.ui.battery_usage.rpb_setValue(batt.percent)
            # set progress bar style
            self.ui.battery_usage.rpb_setBarStyle("Hybrid2")
            # set progress line color
            self.ui.battery_usage.rpb_setLineColor((25, 202, 173))
            # set progress pie color
            self.ui.battery_usage.rpb_setPieColor((160, 238, 225))
            # set progress bar text color
            self.ui.battery_usage.rpb_setTextColor((244, 96, 108))
            # set progress bar starting position
            # north,east,west,south
            self.ui.battery_usage.rpb_setInitialPos('West')
            # set progress bar text type : value or percentage
            # value percentage
            self.ui.battery_usage.rpb_setTextFormat('Percentage')
            # set progress bar font
            # set progress bar width
            self.ui.battery_usage.rpb_setLineWidth(15)
            # path width
            self.ui.battery_usage.rpb_setPathWidth(15)
            # set progress bar line cap
            # roundcap squarecap
            self.ui.battery_usage.rpb_setLineCap('RoundCap')
            # line style
            # dotline dashline
            # self.ui.battery_usage.rpb_setLineStyle('DotLine')

            # SLEEP 1 sec
            time.sleep(1)

    # Style Menu Buttons Styling Function
    def applyButtonStyle(self):
        # reset style for other buttons
        for w in self.ui.menu_frame.findChildren(QPushButton):
            if w.objectName() != self.sender().objectName():
                w.setStyleSheet("border-bottom: none;")

        # apply new style to clicked button
        self.sender().setStyleSheet("border-bottom: 2px solid")
        return

    # slide left menu function
    def slideLeftMenu(self):
        # get current left menu width
        width = self.ui.left_menu_contents_frame.width()

        # if minimized
        if width == 50:
            newWidth = 200
        else:
            newWidth = 50

        # animate the transition
        self.animation = QPropertyAnimation(self.ui.left_menu_contents_frame, b"minimumWidth")
        self.animation.setDuration(250)
        self.animation.setStartValue(width)
        self.animation.setEndValue(newWidth)
        self.animation.setEasingCurve(QEasingCurve.InOutQuart)
        self.animation.start()

    # add mouse events to the window
    def mousePressEvent(self, event):
        self.clickPosition = event.globalPos()

    # update restore button icon on maximizing or minimizing window
    def restore_or_maximize_window(self):
        # if window is maximized
        if self.isMaximized():
            self.showNormal()
            # change icon
            self.ui.restore_window_button.setIcon(QIcon(u":/icons/icons/plus-circle.svg"))
        else:
            self.showMaximized()
            # change icon
            self.ui.restore_window_button.setIcon(QIcon(u":/icons/icons/stop-circle.svg"))


# execute app
if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec_())
